[Py-OO] Aula 03

Modelo de dados do Python

O que você vai aprender nesta aula?

Após o término da aula você terá aprendido:

  • O que é o modelo de dados do Python
  • Para que servem e como funcionam métodos mágicos
  • Protocolos em Python
    • Sequência
  • Sobrecarga de operadores
  • Duck Typing

Este material usou o Capítulo 1 (Modelo de dados do Python) do livro Python Fluente do Luciano Ramalho

Nesta aula vamos falar sobre como funciona o modelo de dados do Python.

O Python é uma linguagem conhecida por sua consistência. Isso permite que, após trabalhar certo tempo com a linguagem, você consiga ter palpiters corretos sobre recursos do Python que você ainda não domina.

Um exemplo da consistência da linguagem se dá pela função len(), que apesar de parecer estranho de se usar - len(collection) ao invés de collection.len() como é feito em outras linguagens - sabemos, conforme visto no curso, que podemos usá-la para qualquer coleção, enquanto outras linguagens possuem métodos de nomes diferentes para realizar essa mesma operação.

O responsável por consistência (e estranheza) é o Python data model (modelo de dados do Python) que descreve a API que pode ser usada para fazer que seus próprios objetos interajam bem com os recursos mais idiomáticos da linguagem. Ele descreve os objetos e como estes interagem entre si.

O modelo de dados formaliza as interfaces dos blocos de construção da própria linguagem, por exemplo, as sequências, os iteradores, as funções, as classes, os gerenciadores de contexto e assim por diante.

O python faz isso usando os métodos especiais: o interpretador do Python chama esses métodos para realizar operações básicas em objetos, geralmente acionados por uma sintaxe especial.

Os métodos especiais são sempre escritos com underscores duplos no início e no fim (como __getitem__). Por exemplo a sintaxe especial obj[chave] é tratada pelo método especial __getitem__. Quando o interpretador avalia colecao[chave] ele chama colecao.__getitem__(chave).

Vamos mostrar um exemplo de como podemos usar o modelo de dados do python a nosso favor. Vamos criar um baralho pythônico:


In [1]:
from exemplos.baralho import Baralho

baralho = Baralho()

Podemos acessar as cartas do baralho por índice:


In [2]:
baralho[0]


Out[2]:
Carta(valor='2', naipe='copas')

In [3]:
baralho[-1]


Out[3]:
Carta(valor='K', naipe='espadas')

Também podemos realizar slicing no baralho:


In [4]:
baralho[:5]


Out[4]:
[Carta(valor='2', naipe='copas'),
 Carta(valor='2', naipe='ouros'),
 Carta(valor='2', naipe='paus'),
 Carta(valor='2', naipe='espadas'),
 Carta(valor='3', naipe='copas')]

In [5]:
baralho[15:20]


Out[5]:
[Carta(valor='5', naipe='espadas'),
 Carta(valor='6', naipe='copas'),
 Carta(valor='6', naipe='ouros'),
 Carta(valor='6', naipe='paus'),
 Carta(valor='6', naipe='espadas')]

In [6]:
baralho[-5:]


Out[6]:
[Carta(valor='Q', naipe='espadas'),
 Carta(valor='K', naipe='copas'),
 Carta(valor='K', naipe='ouros'),
 Carta(valor='K', naipe='paus'),
 Carta(valor='K', naipe='espadas')]

E iterá-lo:


In [7]:
for carta in baralho:
    print(carta)


Carta(valor='2', naipe='copas')
Carta(valor='2', naipe='ouros')
Carta(valor='2', naipe='paus')
Carta(valor='2', naipe='espadas')
Carta(valor='3', naipe='copas')
Carta(valor='3', naipe='ouros')
Carta(valor='3', naipe='paus')
Carta(valor='3', naipe='espadas')
Carta(valor='4', naipe='copas')
Carta(valor='4', naipe='ouros')
Carta(valor='4', naipe='paus')
Carta(valor='4', naipe='espadas')
Carta(valor='5', naipe='copas')
Carta(valor='5', naipe='ouros')
Carta(valor='5', naipe='paus')
Carta(valor='5', naipe='espadas')
Carta(valor='6', naipe='copas')
Carta(valor='6', naipe='ouros')
Carta(valor='6', naipe='paus')
Carta(valor='6', naipe='espadas')
Carta(valor='7', naipe='copas')
Carta(valor='7', naipe='ouros')
Carta(valor='7', naipe='paus')
Carta(valor='7', naipe='espadas')
Carta(valor='8', naipe='copas')
Carta(valor='8', naipe='ouros')
Carta(valor='8', naipe='paus')
Carta(valor='8', naipe='espadas')
Carta(valor='9', naipe='copas')
Carta(valor='9', naipe='ouros')
Carta(valor='9', naipe='paus')
Carta(valor='9', naipe='espadas')
Carta(valor='10', naipe='copas')
Carta(valor='10', naipe='ouros')
Carta(valor='10', naipe='paus')
Carta(valor='10', naipe='espadas')
Carta(valor='A', naipe='copas')
Carta(valor='A', naipe='ouros')
Carta(valor='A', naipe='paus')
Carta(valor='A', naipe='espadas')
Carta(valor='J', naipe='copas')
Carta(valor='J', naipe='ouros')
Carta(valor='J', naipe='paus')
Carta(valor='J', naipe='espadas')
Carta(valor='Q', naipe='copas')
Carta(valor='Q', naipe='ouros')
Carta(valor='Q', naipe='paus')
Carta(valor='Q', naipe='espadas')
Carta(valor='K', naipe='copas')
Carta(valor='K', naipe='ouros')
Carta(valor='K', naipe='paus')
Carta(valor='K', naipe='espadas')

Iterá-lo de trás para frente:


In [8]:
for carta in reversed(baralho):
    print(carta)


Carta(valor='K', naipe='espadas')
Carta(valor='K', naipe='paus')
Carta(valor='K', naipe='ouros')
Carta(valor='K', naipe='copas')
Carta(valor='Q', naipe='espadas')
Carta(valor='Q', naipe='paus')
Carta(valor='Q', naipe='ouros')
Carta(valor='Q', naipe='copas')
Carta(valor='J', naipe='espadas')
Carta(valor='J', naipe='paus')
Carta(valor='J', naipe='ouros')
Carta(valor='J', naipe='copas')
Carta(valor='A', naipe='espadas')
Carta(valor='A', naipe='paus')
Carta(valor='A', naipe='ouros')
Carta(valor='A', naipe='copas')
Carta(valor='10', naipe='espadas')
Carta(valor='10', naipe='paus')
Carta(valor='10', naipe='ouros')
Carta(valor='10', naipe='copas')
Carta(valor='9', naipe='espadas')
Carta(valor='9', naipe='paus')
Carta(valor='9', naipe='ouros')
Carta(valor='9', naipe='copas')
Carta(valor='8', naipe='espadas')
Carta(valor='8', naipe='paus')
Carta(valor='8', naipe='ouros')
Carta(valor='8', naipe='copas')
Carta(valor='7', naipe='espadas')
Carta(valor='7', naipe='paus')
Carta(valor='7', naipe='ouros')
Carta(valor='7', naipe='copas')
Carta(valor='6', naipe='espadas')
Carta(valor='6', naipe='paus')
Carta(valor='6', naipe='ouros')
Carta(valor='6', naipe='copas')
Carta(valor='5', naipe='espadas')
Carta(valor='5', naipe='paus')
Carta(valor='5', naipe='ouros')
Carta(valor='5', naipe='copas')
Carta(valor='4', naipe='espadas')
Carta(valor='4', naipe='paus')
Carta(valor='4', naipe='ouros')
Carta(valor='4', naipe='copas')
Carta(valor='3', naipe='espadas')
Carta(valor='3', naipe='paus')
Carta(valor='3', naipe='ouros')
Carta(valor='3', naipe='copas')
Carta(valor='2', naipe='espadas')
Carta(valor='2', naipe='paus')
Carta(valor='2', naipe='ouros')
Carta(valor='2', naipe='copas')

Enumerá-lo!!!111!!!onze!!11!


In [9]:
for carta in enumerate(baralho):
    print(carta)


(0, Carta(valor='2', naipe='copas'))
(1, Carta(valor='2', naipe='ouros'))
(2, Carta(valor='2', naipe='paus'))
(3, Carta(valor='2', naipe='espadas'))
(4, Carta(valor='3', naipe='copas'))
(5, Carta(valor='3', naipe='ouros'))
(6, Carta(valor='3', naipe='paus'))
(7, Carta(valor='3', naipe='espadas'))
(8, Carta(valor='4', naipe='copas'))
(9, Carta(valor='4', naipe='ouros'))
(10, Carta(valor='4', naipe='paus'))
(11, Carta(valor='4', naipe='espadas'))
(12, Carta(valor='5', naipe='copas'))
(13, Carta(valor='5', naipe='ouros'))
(14, Carta(valor='5', naipe='paus'))
(15, Carta(valor='5', naipe='espadas'))
(16, Carta(valor='6', naipe='copas'))
(17, Carta(valor='6', naipe='ouros'))
(18, Carta(valor='6', naipe='paus'))
(19, Carta(valor='6', naipe='espadas'))
(20, Carta(valor='7', naipe='copas'))
(21, Carta(valor='7', naipe='ouros'))
(22, Carta(valor='7', naipe='paus'))
(23, Carta(valor='7', naipe='espadas'))
(24, Carta(valor='8', naipe='copas'))
(25, Carta(valor='8', naipe='ouros'))
(26, Carta(valor='8', naipe='paus'))
(27, Carta(valor='8', naipe='espadas'))
(28, Carta(valor='9', naipe='copas'))
(29, Carta(valor='9', naipe='ouros'))
(30, Carta(valor='9', naipe='paus'))
(31, Carta(valor='9', naipe='espadas'))
(32, Carta(valor='10', naipe='copas'))
(33, Carta(valor='10', naipe='ouros'))
(34, Carta(valor='10', naipe='paus'))
(35, Carta(valor='10', naipe='espadas'))
(36, Carta(valor='A', naipe='copas'))
(37, Carta(valor='A', naipe='ouros'))
(38, Carta(valor='A', naipe='paus'))
(39, Carta(valor='A', naipe='espadas'))
(40, Carta(valor='J', naipe='copas'))
(41, Carta(valor='J', naipe='ouros'))
(42, Carta(valor='J', naipe='paus'))
(43, Carta(valor='J', naipe='espadas'))
(44, Carta(valor='Q', naipe='copas'))
(45, Carta(valor='Q', naipe='ouros'))
(46, Carta(valor='Q', naipe='paus'))
(47, Carta(valor='Q', naipe='espadas'))
(48, Carta(valor='K', naipe='copas'))
(49, Carta(valor='K', naipe='ouros'))
(50, Carta(valor='K', naipe='paus'))
(51, Carta(valor='K', naipe='espadas'))

Sorteio de cartas usando o módulo random:


In [10]:
from random import choice

choice(baralho)


Out[10]:
Carta(valor='9', naipe='ouros')

In [11]:
choice(baralho)


Out[11]:
Carta(valor='7', naipe='copas')

In [12]:
choice(baralho)


Out[12]:
Carta(valor='K', naipe='paus')

Sorteando 5 cartas (pode haver repetição):


In [13]:
mao = [choice(baralho) for _ in range(5)]
mao


Out[13]:
[Carta(valor='6', naipe='espadas'),
 Carta(valor='3', naipe='ouros'),
 Carta(valor='10', naipe='espadas'),
 Carta(valor='4', naipe='espadas'),
 Carta(valor='K', naipe='espadas')]

Também podemos verificar se uma carta específica está no baralho:


In [14]:
from exemplos.baralho import Carta

Carta('10', 'espadas') in baralho


Out[14]:
True

In [15]:
Carta('3', 'alabardas') in baralho


Out[15]:
False

E se saber quantas cartas há no baralho:


In [16]:
len(baralho)


Out[16]:
52

Você deve estar se perguntando quanto custou para implementar tudo isso? Respota: muito pouco.

""" Arquivo: 02-python-oo/aula-03/exemplos/baralho.py """

from collections import namedtuple


Carta = namedtuple('Carta', ['valor', 'naipe'])


class Baralho:
    valores = [str(n) for n in range(2, 11)] + list('AJQK')
    naipes = 'copas ouros paus espadas'.split()

    def __init__(self):
        self.cartas = [Carta(v, n) for v in self.valores for n in self.naipes]

    def __len__(self):
        return len(self.cartas)

    def __getitem__(self, pos):
        return self.cartas[pos]

Métodos especiais

Vimos duas vantagens de usar os métodos especiais para tirar proveito do modelo de dados do Python:

  • Os usuarios de suas classes não precisarão memorizar nomes arbitrários de métodos para realizar operações comuns (Como obter a quantidade de itens? Uso .size(), .length(), ou o quê?)

  • Podemos se beneficiar da biblioteca-padrão do Python e não reinventar a roda, como visto no uso das funções random.choice e reversed.

Os métodos especiais foram criados para serem chamados pelo interpretador Python e não diretamente. Não usamos objeto.__len__, para obter a quantidade de elementos, mas sim len(objeto). Se objeto for a instância de uma classe definida pelo usuário (programador), o Python chamará o método __len__ da instância.

Na grande maioria das vezes a chamada aos métodos especiais será feita de forma implícita. Por exemplo, a construção de for i in x invoca iter(x), que poderá chamar x.__iter__() se existir.

Um exemplo comum de implementação e chamada de métodos especiais diretamente é o __init__ para sobrescrever o inicilizador da superclasse. Também é comum invocar o inicializador da superclasse diretamente com, por exemplo, super().__init__() ao implementar seu próprio inicializador.

Caso precise chamar um método especial, em geral é muito melhor chamar a função embutida relacionada ou a sintaxe especial (obj[chave], len, iter, str etc.). Essas funções embutidas invocam o método especiail correspondente, porém, com frequência, oferecem outros serviços e - para os tipos embutidos - são mais rápidas que chamadas de métodos.

#TODO

Monkey patching em __setitem__

Protocolo de Sequência

Nós podemos fazer todas essas operações no Baralho sem herdar de alguma classe espeicial, pois implementamos o protocolo de sequência como definido no modelo de dados do Python. Agora ficam duas dúvidas: o que é exatamente um protocolo e uma sequência?

No contexto de programação orientada a objetos um protocolo é uma interface informal definida somente na documentação e não no código. Por exemplo, o protocolo de sequência em Python implica somente os métodos __len__ e __getitem__. Qualquer classe que implemente esses métodos poderá ser usada em qualquer lugar em que se espera uma sequência.

Esse tipo de programação ficou conhecida como Duck Typing e é muito comum em linguagens dinâmicas como Python e Ruby.

A duck typing (pode ser traduzido como: tipagem pato ou pato digitando)

"Não verifique se é um pato: verifique se faz quack como um pato, anda como um pato etc., de acordo com o subconjunto exatao de comportamento de pato de que você precisa para usar a linguagem." (Alex Martelli, 2000)

Essa técnica consiste em não verificar se uma classe é, por exemplo, uma sequência e sim se ela se comporta como uma sequência.

É importante notar que, como os protocolos são informais e não impostos. Geralmente você pode implementar somente a parte de um protocolo que faz sentido a sua aplicação sem que haja problemas. Por exemplo, para dar suporte a iteração é necessário implementar somente o método __getitem__ e não é necessário o __len__

Agora que sabemos como funcionam os protocolos em Python, vamos falar sobre o protocolo de sequência.

O python data model define sequências como conjuntos finitos indexados por números não negativos. Sendo n o tamanho da sequência, os índices vão de 0 a n - 1 e são acessados por a[i].

Falaremos mais sobre o protocolo de sequência futuramente. Caso queira entender mais sobre o assunto consulte sua documentação.

Emulando tipos numéricos

Vamos ver como utilizar os métodos especiais para emular tipos numéricos.

O python data model diz que números são criados por números declará-los em sua forma literal (como por exemplo a = 3, 3.4 etc.) e resultados de operações aritméticos e funções aritméticas embutidas.

Implementaremos uma classe para representar vetores bidimensionais (vetores euclidianos) usados na matemática e na física.


In [17]:
from exemplos.vetor1 import Vetor

v1 = Vetor(1, -2)
v2 = Vetor(3, 4)

Podemos somar vetores usando o operador +:


In [18]:
v1 + v2


Out[18]:
Vetor(4, 2)

Usar o operador de subtração:


In [19]:
v1 - v2


Out[19]:
Vetor(-2, -6)

Multiplicação por escalar:


In [20]:
v1 * 3


Out[20]:
Vetor(3, -6)

In [21]:
v2 * -4


Out[21]:
Vetor(-12, -16)

Valor absoluto (distância do vetor até a origem):


In [22]:
abs(v2)


Out[22]:
5.0

Comparação de vetores por valor:


In [23]:
v1 == v2


Out[23]:
False

In [24]:
v1 == Vetor(1, -2)


Out[24]:
True

In [25]:
v2 == Vetor(3, 4)


Out[25]:
True

Podemos fazer verificações booleanas com o vetor:


In [26]:
if v1:
    print('v1 existe e possui valor')


v1 existe e possui valor

In [27]:
if not Vetor(0, 0):
    print('vetor não possui valor')
else:
    print('alguma coisa deu errado')


vetor não possui valor

Esse exemplo usou a classe Vetor demonstrada a seguir, que implementa as operações demonstradas por meio dos métodos especiais __repr__, __abs__, __add__, __bool__, __eq__, __sub__ e __mul__:

"""
Arquivo: 02-python-oo/aula-03/exemplos/vetor.py

Implementa um vetor bidimensional
"""
import math


class Vetor:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __repr__(self):
        return 'Vetor({!r}, {!r})'.format(self.x, self.y)

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __add__(self, v2):
        return Vetor(self.x + v2.x, self.y + v2.y)

    def __bool__(self):
        return bool(self.x or self.y)

    def __eq__(self, v2):
        return self.x == v2.x and self.y == v2.y

    def __sub__(self, v2):
        return Vetor(self.x - v2.x, self.y - v2.y)

    def __mul__(self, scalar):
        return Vetor(self.x * scalar, self.y * scalar)

O método __repr__ é responsável por retornar a representação do objeto para inspeção. Esse valor é usado no modo interativo e em debugers. Caso esse método não seja sobrescrito será exibido algo como <Vetor object at 0x123e9230>.

A representação do objeto é obtido a partir da função embutida repr(). É uma boa prática usar !r para obter a representação dos atributos do objeto, pois mostra a diferença fundamental entre Vector(1, 2) e Vector('1', '2') - a última não funcionará, pois os argumentos do construtor devem ser número e não str.

Também há o método __str__ que é utilizado para exibir o valor do objeto para o usuário final. Para entender melhor a diferença consulte esta thread do stack overflow que foi muito bem respondida pelos pythonistas Alex Martelli e Martijn Peters.

Esse exemplo contém alguns problemas:


In [28]:
v1


Out[28]:
Vetor(1, -2)

In [29]:
v1 * 5


Out[29]:
Vetor(5, -10)

In [30]:
5 * v1


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-30-e463c89cb0af> in <module>()
----> 1 5 * v1

TypeError: unsupported operand type(s) for *: 'int' and 'Vetor'

No exemplo anterior tentamos multiplicar um int por um Vetor, porém foi levantada uma exceção já que o tipo int não sabe multiplicar por Vetor. Apenas Vetor sabe multiplicar por escalar.

Para resolver esse problema precisamos antes entender como funciona x * y:

  1. Se x tiver x.__mul__, chama x.__mul__(y) e devolve o resultado a menos que seja NotImplemented
  2. Se x não tiver x.__mul__, ou sua chamada devolver NotImplemented, verifica se y possui __rmul__, chama y.__rmul__(x) e devolve o resultado, a menos que seja NotImplemented
  3. Se y não tiver __rmul__, ou sua chamada devolver NotImplented, levanta TypeError com uma mensagem unsupported operand type(s)

O método __rmul__ é chamado de versão refletida, reversa ou direita (do inglês right) de __mul__.

Para corrigir precisamos adicionar o método __rmul__ à clase Vetor:

import math

class Vetor:
    ...

    def __mul__(self, escalar):
        return Vetor(self.x * escalar, self.y * escalar)

    def __rmul__(self, outro):
        return self * outro

Porém, ao adicionar esse código acontece outro problema:


In [33]:
from exemplos.vetor2 import Vetor

Vetor(1, 2) * Vetor(2, 4)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-33-3a0d23b5f017> in <module>()
      1 from exemplos.vetor2 import Vetor
      2 
----> 3 Vetor(1, 2) * Vetor(2, 4)

TypeError: unsupported operand type(s) for *: 'Vetor' and 'Vetor'

Esse resultado não faz sentido, não é assim que multiplicação de vetores funciona.

Não vamos implementar aqui a multiplicação de vetores, pois o foco da aula é ensinar programação e não matemática. Portanto, precisamos permitir a multiplicação de vetores apenas por escalares, vamos corrigir a função __mul__:

import math
from numbers import Number

class Vetor:
    ...

    def __mul__(self, escalar):
        if isinstance(escalar, Real):
            return Vetor(self.x * escalar, self.y * escalar)
        else:
            return NotImplemented

Verificamos se o escalar recebido de fato é um número real, se for retornamos o resultado da multiplicação do vetor pelo escalar, caso contrário é retornado NotImplemented.

Retornamos NotImplemented ao invés de levantar uma exceção, para permitir que o Python tente executar __rmul__ no escalar, pois pode ser que seja algum tipo que implemente a operação reversa da multiplicação.

Também há um problema com a comparação de valores quando comparamos vetores com outros tipos:


In [34]:
from exemplos.vetor1 import Vetor

Vetor(1, 3) == [1, 2]


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-34-77c91994217a> in <module>()
      1 from exemplos.vetor1 import Vetor
      2 
----> 3 Vetor(1, 3) == [1, 2]

/home/luiz/talks/trilha/02-python-oo/aula-03/exemplos/vetor1.py in __eq__(self, outro)
     23 
     24     def __eq__(self, outro):
---> 25         return self.x == outro.x and self.y == outro.y
     26 
     27     def __sub__(self, outro):

AttributeError: 'list' object has no attribute 'x'

In [35]:
Vetor(2, 4) == 'oi'


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-35-41a211be8d0e> in <module>()
----> 1 Vetor(2, 4) == 'oi'

/home/luiz/talks/trilha/02-python-oo/aula-03/exemplos/vetor1.py in __eq__(self, outro)
     23 
     24     def __eq__(self, outro):
---> 25         return self.x == outro.x and self.y == outro.y
     26 
     27     def __sub__(self, outro):

AttributeError: 'str' object has no attribute 'x'

Essa comparação deveria retornar False, não levantar uma exceção. Podemos corrigir esse problema da seguinte maneira:

import math
from numbers import Number

class Vetor:
    ...

    def __eq__(self, outro):                              
        if isinstance(outro, Vetor):                      
            return self.x == outro.x and self.y == outro.y
        else:                                             
            return NotImplemented

Agora podemos comparar Vetor com outros tipos:


In [2]:
from exemplos.vetor2 import Vetor

Vetor(1, 3) == 8


Out[2]:
False

In [3]:
Vetor(-2, 3) == [1, 2, 3]


Out[3]:
False

Nosso vetor ainda não suporta operações unárias como -v e +v:


In [10]:
from exemplos.vetor1 import Vetor

-Vetor(1, 5)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-b247c8b4fd1e> in <module>()
      1 from exemplos.vetor1 import Vetor
      2 
----> 3 -Vetor(1, 5)

TypeError: bad operand type for unary -: 'Vetor'

In [11]:
+Vetor(2, 3)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-11-7d566f2c6e0b> in <module>()
----> 1 +Vetor(2, 3)

TypeError: bad operand type for unary +: 'Vetor'

Para que esses operadores funcionem precisamos definir os métodos __neg__ e __pos__:

from numbers import Real
import math


class Vetor:
    ...

    def __neg__(self):
        return self * -1

    def __pos__(self):
        return self

A função __neg__ simplesmente retornou o vetor por -1. Já a função __pos__ retorna a própria instância, pois +Vetor(x, y) é sempre igual a ele mesmo Vetor(x, y).

Seria interessante se pudessemos desempacotar os valores de x e de y de um vetor para uma tupla. Isso facilitaria nossa vida, pois poderiamos fazer isso:


In [13]:
v = Vetor(3, -1)

In [16]:
x, y = v
x, y


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-16-c2ef2c058e4f> in <module>()
----> 1 x, y = v
      2 x, y

TypeError: 'Vetor' object is not iterable

Ao invés disso:


In [17]:
x = v.x
y = v.y
x, y


Out[17]:
(3, -1)

O desempacotamento facilita ainda mais nossa vida se tivessemos uma lista de vetores:


In [20]:
from random import randint

lista_vetores = [Vetor(x=randint(-10, 10), y=randint(-10, 10)) for _ in range(5)]
lista_vetores


Out[20]:
[Vetor(7, -8), Vetor(0, -5), Vetor(-6, 0), Vetor(0, 2), Vetor(6, -8)]

Pois poderiamos ter acesso facilitado a x e y durante uma iteração usando o desempacotamento de sequências:


In [21]:
for x, y in lista_vetores:
    print(x, y)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-21-680936c471bd> in <module>()
----> 1 for x, y in lista_vetores:
      2     print(x, y)

TypeError: 'Vetor' object is not iterable

Ao invés de ter que acessar os atributos diretamente:


In [23]:
for vetor in lista_vetores:
    print(vetor.x, vetor.y)


7 -8
0 -5
-6 0
0 2
6 -8

Antes de implementar essa funcionalidade precisamos entender como funciona o desempacotamento de sequências: o objeto a direita é iterado e cada variável a esquerda é atribuída ao item resultante dessa iteração.


In [30]:
(a, b) = (1, 0)
a, b


Out[30]:
(1, 0)

In [27]:
[a, b] = [3, 4]
a, b


Out[27]:
(3, 4)

O que acontece por trás de tudo isso é: Extraímos o iterador da sequência a direita:


In [33]:
iterador = iter([3, 4])

Ele é iterado uma vez e o resultado da iteração é atribuído a primeira variável:


In [ ]:
a = next(iterador)

E é iterado até chegar ao último elemento:


In [34]:
b = next(iterador)

In [35]:
a, b


Out[35]:
(1, 3)

Agora que sabemos disso fica claro que, para nosso vetor ser desempacotado precisamos torná-lo iterável. Para isso podemos definir o método __iter__ que deve retornar um iterador:

...

class Vetor:
    ...

    def __iter__(self):
        return iter((self.x, self.y))

Nesse método definimos uma tupla composta pelos atributos x e y da instância do Vetor e retornamos o iterador da dessa tupla.

Essa implementação funciona, porém podemos usar geradores para deixar esse método mais simples e eficiente:

...

class Vetor:
    ...

    def __iter__(self):
        yield self.x; yield self.y

Para entender essa implementação é necessário conhecer o funcionamento de geradores, que veremos numa aula futura.

As operações que implementamos para nosso vetor não o alteram, mesmo quando usamos operadores acumulados:


In [36]:
from exemplos.vetor2 import Vetor

v = Vetor(1, 2)
v, id(v)


Out[36]:
(Vetor(1, 2), 140691520740152)

Se realizarmos uma soma acumulada com outro vetor:


In [37]:
v += Vetor(2, 3)
v, id(v)


Out[37]:
(Vetor(3, 5), 140691520740320)

Um novo objeto é criado, pois o objeto referenciado pela variável v não é mais o mesmo (a identidade dos objetos são diferentes).

Para que essas operações de fato modifiquem um objeto, como acontecem com objetos mutáveis:


In [38]:
lista = [1, 2, 3, 4]
lista, id(lista)


Out[38]:
([1, 2, 3, 4], 140691520641416)

In [39]:
lista += [5, 6, 7, 8]
lista, id(lista)


Out[39]:
([1, 2, 3, 4, 5, 6, 7, 8], 140691520641416)

O objeto permanece o mesmo, seu valor que é alterado.

Matemáticamente não faz muito sentido ter um vetor mutável, mas para entendermos melhor esses conceitos vamos fazer um VetorMutável, como subclasse de Vetor, que altere o valor do vetor quanto as operações += e *= forem usadas implementando os métodos __iadd__ e __imul__:


In [42]:
from exemplos.vetor2 import VetorMutavel

vm = VetorMutavel(2, 3)
vm, id(vm)


Out[42]:
(VetorMutavel(2, 3), 140691520738752)

In [43]:
vm += VetorMutavel(-1, 4)
vm, id(vm)


Out[43]:
(VetorMutavel(1, 7), 140691520738752)

In [44]:
vm *= -2
vm, id(vm)


Out[44]:
(VetorMutavel(-2, -14), 140691520738752)

Essa classe VetorMutavel pode ser implementada da seguinte maneira:

class VetorMutavel(Vetor):

    def __iadd__(self, outro):
        if isinstance(outro, Vetor):
            self.x += outro.x 
            self.y += outro.y
            return self
        return NotImplemented

    def __imul__(self, outro):
        if isinstance(outro, Real):
            self.x *= outro
            self.y *= outro
            return self
        return NotImplemented

Emulando tipos chamáveis

Chamáveis são os tipos que podem ser chamados ou invocados quando escrevemos seu nome seguido por parentesis e podem receber argumentos. São chamáveis:

  • Funções:

In [23]:
def chamavel():
    print('posso ser chamado')

In [2]:
chamavel()


posso ser chamado

In [22]:
type(chamavel)


Out[22]:
function
  • Classes

In [3]:
class Foo:
    def bar(self):
        print('também posso ser chamado!')

Quando classes são chamadas retornam instâncias:


In [6]:
foo = Foo()
  • Métodos de instâncias

In [7]:
foo.bar()


também posso ser chamado!
  • Geradores:

In [8]:
def gen():
    yield 1

Chamar geradores retorna objetos geradores que executam o código definido


In [11]:
gen()


Out[11]:
<generator object gen at 0x7f0e343eeeb8>

Para acessar o conteúdo do gerador precisamos iterá-lo, para isso podemos usar a função embutida next() :


In [21]:
g = gen()
next(g)


Out[21]:
1

Porém se requisitamos mais valores de um gerador que ele pode gerar uma exceção é levantada:


In [20]:
next(g)


---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-20-5f315c5de15b> in <module>()
----> 1 next(g)

StopIteration: 

Veremos mais sobre geradores nas próximas aulas. Para saber mais sobre chamáveis consulte o python data model e como chamáveis são expressados

  • Objetos

Por fim, objetos que definem um método __call__ também são chamáveis.

Para demonstrar isso vamos implementar uma tombola (gaiola de bingo). A tombola pode:

  • Carregar itens
  • Inspecionar itens da tombola
  • Verificar se está vazia
  • Misturar itens
  • Sortear um item
  • Sortear um item chamando a instância da tombola

Vamos ao código:


In [79]:
import random


class Tombola:
    def __init__(self, itens=None):
        self._itens = []
        self.carrega(itens)
        
    def __call__(self):
        return self.sorteia()

    def carrega(self, itens):
        self._itens.extend(itens)

    def inspeciona(self):
        return tuple(self._itens)

    def mistura(self):
        random.shuffle(self._itens)

    def sorteia(self):
        return self._itens.pop()

    def vazia(self):
        return len(self._itens) == 0

Vamos criar nossa tombola que armazena os números de 1 a 20:


In [26]:
tombola = Tombola(range(1, 21))

Verificaremos seus itens:


In [27]:
tombola.inspeciona()


Out[27]:
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

Está vazia?


In [28]:
tombola.vazia()


Out[28]:
False

Misturando:


In [29]:
tombola.mistura()
tombola.inspeciona()


Out[29]:
(8, 2, 1, 13, 12, 6, 15, 20, 18, 5, 3, 17, 10, 14, 16, 9, 11, 4, 19, 7)

Sorteando um item da maneira clássica:


In [30]:
tombola.sorteia()


Out[30]:
7

Aproveitando o método __call__ que definimos podemos sortear chamado o objeto tombola sem chamar o método tombola.sorteia():


In [31]:
tombola()


Out[31]:
19

Os operadores "aritméticos" (como +, * etc.) também podem ser aplicados a outros objetos para realizar operações que fazem sentido a esse objetos. Como por exemplo em listas e strings, em que o operador + realiza a concatenação:


In [32]:
lista = [1, 2, 3, 4]
lista


Out[32]:
[1, 2, 3, 4]

In [33]:
lista + [5, 6, 7, 8]


Out[33]:
[1, 2, 3, 4, 5, 6, 7, 8]

In [34]:
[-4, -3, -2, -1, 0] + lista


Out[34]:
[-4, -3, -2, -1, 0, 1, 2, 3, 4]

In [35]:
pal = "palavra"
pal


Out[35]:
'palavra'

In [37]:
pal + '!!!1!1!11onze!!!1!'


Out[37]:
'palavra!!!1!1!11onze!!!1!'

Para mostrar como isso funciona vamos implementar uma tombola expansível que torne possível juntar os itens dessa tombola com outra tombola ou um iterável.

Vamos definir o método __add__ para permitir a "soma" de tombolas:


In [39]:
class TombolaExpansivel(Tombola):
    def __add__(self, other):
        if isinstance(other, Tombola):
            return TombolaExpansivel(self.inspeciona() + other.inspeciona())
        else:
            return NotImplemented

Na linha 3 verificamos se o objeto somado é uma instância de Tombola, isso permite que nossa TombolaExpansivel seja somada com Tombola e todas suas subclasses:


In [62]:
tombola_exp = TombolaExpansivel(range(1, 11))
tombola_exp.inspeciona()


Out[62]:
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Podemos somar a instância de TombolaExpansivel com Tombola e suas subclasses:


In [44]:
outra_tombola = tombola_exp + Tombola(range(11, 21))
outra_tombola.inspeciona()


Out[44]:
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

In [57]:
mais_tombolas = tombola_exp + TombolaExpansivel(range(11, 16))
mais_tombolas.inspeciona()


Out[57]:
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)

Sobrescrevendo o método __add__ já é possível usar a soma atribuída, porém haverá um problema indesejado:


In [58]:
id(tombola_exp), tombola_exp.inspeciona()


Out[58]:
(139698982480304, (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))

In [63]:
tombola_exp += Tombola(range(11, 16))
id(tombola_exp), tombola_exp.inspeciona()


Out[63]:
(139698982479800, (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15))

Vemos que as identidades dos objetos atribuidos a tombola_exp são diferentes. Isso por que a atribuição acumulada, por padrão, na verdade faz:


In [64]:
tombola_exp = tombola_exp + Tombola(range(16, 21))

In [65]:
tombola_exp.inspeciona()


Out[65]:
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

E nossa função o __add__ cria um novo objeto. Como queremos que nossa TombolaExpansivel seja mutável, precisamos definir o método __iadd__ para modificar a instância.

Aproveitando que vamos mexer no __iadd__ podemos melhorar nossa tombola para receber item de qualquer iterável e não somente de Tombola e suas subclasses:


In [80]:
class TombolaExpansivel(Tombola):
    def __add__(self, other):
        if isinstance(other, Tombola):
            return TombolaExpansivel(self.inspeciona() + other.inspeciona())
        else:
            return NotImplemented
    
    def __iadd__(self, outro):
        if isinstance(outro, Tombola):
            outro_iteravel = outro.inspeciona()
        else:
            try:
                outro_iteravel = iter(outro)
            except TypeError:
                msg = "operando da direita no += deve ser {!r} ou um iterável"
                raise TypeError(msg.format(type(self).__name__))
        self.carrega(outro_iteravel)
        return self

Linha 9 e 10: se o objeto à direita for uma tombola inspecionamos e "pegamos" seus itens

Linha 12 e 13: tenta extrair um iterável do objeto a direita, isso funcionará se este objeto for iterável, se não uma exceção do tipo TypeError é levantada.

Linha 14, 15 e 16: Se for levantada uma exceção TypeError é criada uma outra exceção do tipo TypeError, porém com uma mensagem de erro mais clara.

Linha 17: carrega os próprios itens e da outra tupla.

Agora podemos, de fato, modificar nossa TombolaExpansível:


In [81]:
tombola_exp = TombolaExpansivel(range(-10, 1, 1))
id(tombola_exp), tombola_exp.inspeciona()


Out[81]:
(139698982617496, (-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0))

In [82]:
tombola_exp += [1, 2, 3, 4]
id(tombola_exp), tombola_exp.inspeciona()


Out[82]:
(139698982617496, (-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4))

Para finalizar, vamos ver uma tabela todos os métodos especiais do Python. Algum dos métodos especiais ainda não foram explicados no curso e outros não serão, portanto consulte a documentação caso você precise deles.

Tabela de métodos mágicos

Esta tabela foi tirada do livro Fluent Python (Python Fluente)

Na tabela a seguir constam todos os métodos mágicos por tipo. (em inglês, pois não há ebook da versão pt-br)

Fim da aula 03